﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace MonoStrategy
{
    [XmlRoot("Map")]
    public class XMLMapFile
    {
        [XmlAttribute]
        public String Name { get; set; }
        public XMLImageFile ImageFile { get; set; }

        public class XMLImageFile
        {
            [XmlAttribute]
            public String Source { get; set; }

            public XMLColorDefinition Grass { get; set; }
            public XMLColorDefinition Water { get; set; }
            public XMLColorDefinition Rock { get; set; }
            public XMLColorDefinition Desert { get; set; }
            public XMLColorDefinition Swamp { get; set; }
            public XMLColorDefinition Mud { get; set; }
            public XMLColorDefinition Sand { get; set; }
            public XMLColorDefinition Stone { get; set; }
            public XMLColorDefinition Wood { get; set; }
            public XMLColorDefinition Spot { get; set; }

            public struct XMLColorDefinition
            {
                [XmlAttribute]
                public byte R;
                [XmlAttribute]
                public byte G;
                [XmlAttribute]
                public byte B;
                public int ColorARGB { get { return unchecked((int)0xFF000000 | (((int)R) << 16) | (((int)G) << 8) | B); } }
            }
        }
    }

    internal enum GroundType
    {
        Desert = 0,
        Grass = 1,
        Mud = 2,
        Rock = 3,
        Sand = 4,
        Spot = 5,
        Stone = 6,
        Swamp = 7,
        Water = 8,
        Wood = 9,
        Snow = 10,
    }

    internal class XMLTerrainGenerator : AbstractTerrainGenerator
    {
        public String Directory { get; private set; }
        public String XMLFile { get; private set; }

        public static void Run(GameMap inMap, long inSeed, String inMapFile)
        {
            new XMLTerrainGenerator(inMap, inSeed, inMapFile);
        }

        private XMLTerrainGenerator(GameMap inMap, long inSeed, String inMapFile)
            : base(inMap, inSeed)
        {
            XMLFile = Path.GetFullPath(inMapFile);
            Directory = Path.GetDirectoryName(XMLFile);

            // create and analyze map file
            XmlSerializer format = new XmlSerializer(typeof(XMLMapFile));
            FileStream stream = new FileStream(XMLFile, FileMode.Open, FileAccess.Read, FileShare.Read);
            XMLMapFile xmlMap;

            using (stream)
            {
                xmlMap = (XMLMapFile)format.Deserialize(stream);
            }

            if (String.IsNullOrEmpty(xmlMap.Name))
                throw new ArgumentException();

            if (xmlMap.ImageFile != null)
            {
                GenerateFromImageFile(xmlMap.ImageFile);
            }
            else
            {
                // currently only ImageFile is supported...
                throw new ArgumentException();
            }

            Map.Routing.Analyze();
        }

        private unsafe void GenerateFromImageFile(XMLMapFile.XMLImageFile xmlImgFile)
        {
            xmlImgFile.Source = Path.GetFullPath(Directory + "/" + xmlImgFile.Source);

            try
            {
                if (!File.Exists(xmlImgFile.Source))
                    throw new FileNotFoundException("Unable to locate map image file.");

                Bitmap image = (Bitmap)Bitmap.FromFile(xmlImgFile.Source);
                BitmapData imageData = image.LockBits(new System.Drawing.Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
                GroundType[][] groundLayer = new GroundType[Size][];
                GroundType[][] resourceLayer = new GroundType[Size][];
                bool[][] waterBorders = new bool[Size][];
                double[] heightMap = new double[Size * Size];
                double[] blurBuffer = new double[Size * Size];
                double[] groundMap = new double[Size * Size];
                int size = Size;
                unsafe
                {
                }
                try
                {
                    int* pixels = (int*)imageData.Scan0.ToPointer();
                    int imgWidth = image.Width;
                    double xImgStep = (double)image.Width / (double)Map.Size;
                    double yImgStep = (double)image.Height / (double)Map.Size;
                    double xImg = xImgStep, yImg = 0;
                    int[] knownColors = new int[] 
                    { 
                        xmlImgFile.Desert.ColorARGB,
                        xmlImgFile.Grass.ColorARGB,
                        xmlImgFile.Mud.ColorARGB,
                        xmlImgFile.Rock.ColorARGB,
                        xmlImgFile.Sand.ColorARGB,
                        xmlImgFile.Spot.ColorARGB,
                        xmlImgFile.Stone.ColorARGB,
                        xmlImgFile.Swamp.ColorARGB,
                        xmlImgFile.Water.ColorARGB,
                        xmlImgFile.Wood.ColorARGB,
                    };

                    // infere ground type layer by replacing resources with nearest ground type
                    GroundType prevGroundType = GroundType.Water;
                    int waterColor = xmlImgFile.Water.ColorARGB;

                    for (int x = 0; x < size; x++)
                    {
                        groundLayer[x] = new GroundType[size];
                        resourceLayer[x] = new GroundType[size];
                        waterBorders[x] = new bool[size];
                        yImg = 0;

                        for (int y = 0; y < size; y++)
                        {
                            int pixel = pixels[((int)xImg) + ((int)yImg) * imgWidth];
                            int colorIndex = knownColors.IndexOf(pixel);
                            GroundType groundType;
                            GroundType resType = GroundType.Water;

                            if (colorIndex < 0)
                            {
                                groundType = prevGroundType;
                            }
                            else if ((colorIndex == (int)GroundType.Wood) || (colorIndex == (int)GroundType.Stone) || (colorIndex == (int)GroundType.Spot))
                            {
                                groundType = prevGroundType;
                                resType = (GroundType)colorIndex;
                            }
                            else
                            {
                                groundType = prevGroundType = (GroundType)colorIndex;

                                if (groundType == GroundType.Rock)
                                    resType = GroundType.Rock;
                            }

                            resourceLayer[x][y] = resType;
                            groundLayer[x][y] = groundType;

                            yImg += yImgStep;
                        }
                        xImg += xImgStep;
                    }

                    for (int x = 1; x < size - 1; x++)
                    {
                        for (int y = 1; y < size - 1; y++)
                        {
                            if (groundLayer[x][y] == GroundType.Water)
                            {
                                // if cell has non-water neightbors, we need to mark it as water border!
                                if ((groundLayer[x + 1][y] != GroundType.Water) || (groundLayer[x + 1][y + 1] != GroundType.Water) || (groundLayer[x][y + 1] != GroundType.Water)
                                        || (groundLayer[x][y - 1] != GroundType.Water) || (groundLayer[x - 1][y - 1] != GroundType.Water) || (groundLayer[x - 1][y] != GroundType.Water)
                                        || (groundLayer[x - 1][y + 1] != GroundType.Water) || (groundLayer[x + 1][y - 1] != GroundType.Water))
                                    waterBorders[x][y] = true;
                            }
                        }
                    }
                }
                finally
                {
                    image.UnlockBits(imageData);
                }

                // derive height map from ground type layer
                double waterHeight = -1.0;// (Terrain.Config.Levels[0].Margin + Terrain.Config.Levels[1].Margin) / 2.0;
                double sandHeight = (Terrain.Config.Levels[1].Margin + Terrain.Config.Levels[2].Margin) / 2.0;
                double grassHeight = (Terrain.Config.Levels[2].Margin + Terrain.Config.Levels[3].Margin) / 2.0;
                double rockHeight = (Terrain.Config.Levels[3].Margin + Terrain.Config.Levels[4].Margin) / 2.0;
                double snowHeight = (Terrain.Config.Levels[4].Margin + Terrain.Config.Levels[5].Margin) / 2.0;

                for (int y = 0, offset = 0; y < size; y++)
                {
                    for (int x = 0; x < size; x++)
                    {
                        double height;

                        switch (groundLayer[x][y])
                        {
                            case GroundType.Sand: height = sandHeight; break;
                            case GroundType.Desert:
                            case GroundType.Grass:
                            case GroundType.Mud:
                            case GroundType.Swamp: height = grassHeight; break;
                            case GroundType.Rock: height = rockHeight; break;
                            case GroundType.Snow: height = snowHeight; break;
                            default: height = waterHeight; break;

                        }

                        blurBuffer[offset] = height;
                        heightMap[offset] = waterHeight;

                        offset++;
                    }
                }

                Blur2D(blurBuffer, heightMap, new double[] { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1 });

                Array.Copy(heightMap, groundMap, groundMap.Length);

                // further postprocess heightmap
                PerlinNoise selector = new PerlinNoise(16, 0.18, 0.1, 100000);
                PerlinNoise rockSelector = new PerlinNoise(16, 0.10, 0.2, 100000);
                double[] vec = new double[2];

                for (int y = 0, offset = 0; y < size; y++)
                {
                    for (int x = 0; x < size; x++)
                    {
                        double height = heightMap[offset];

                        vec[0] = x;
                        vec[1] = y;

                        //if (height > rockMargin)
                        //    height = Math.Max(rockMargin, height - rockSelector.Perlin_noise_2D(vec));
                        //else
                        height = Math.Max(-1.0, Math.Min(1.0, height + selector.Perlin_noise_2D(vec)));

                        blurBuffer[offset] = height;

                        offset++;
                    }
                }

                Array.Copy(blurBuffer, heightMap, heightMap.Length);

                for (int x = 0, offset=0; x < Size; x++)
                {
                    for (int y = 0; y < Size; y++)
                    {
                        blurBuffer[offset++] = (waterBorders[x][y]) ? 1 : 0;
                    }
                }

                Save(blurBuffer);

                for (int y = 0, offset = 0; y < Size; y++)
                {
                    for (int x = 0; x < Size; x++, offset++)
                    {
                        double height = heightMap[offset];

                        Terrain.SetHeightAt(x, y, (byte)(128 + 127 * height));
                        Terrain.SetGroundAt(x, y, (byte)(128 + 127 * groundMap[offset]));

                        if(waterBorders[x][y])
                            Terrain.SetWallAt(x, y, WallValue.WaterBorder);

                        switch (resourceLayer[x][y])
                        {
                            case GroundType.Rock: break;
                            case GroundType.Stone: Terrain.SetFlagsAt(x, y, TerrainCellFlags.Stone); break;
                            case GroundType.Wood: Terrain.SetFlagsAt(x, y, TerrainCellFlags.Tree_01); break;
                            case GroundType.Spot: Terrain.AddSpot(new Point(x, y)); break;
                        }
                    }
                }

                if (Terrain.Spots.Count() == 0)
                    throw new ArgumentException("A map needs to provide at least one valid spot.");
            }
            catch (Exception e)
            {
                throw new ArgumentException("Unable to load map file \"" + XMLFile + "\". Inner exception: " + e.ToString());
            }
        }

        unsafe public static void Save(byte[] inData)
        {
            double[] data = new double[inData.Length];

            for (int x = 0; x < data.Length; x++)
            {
                data[x] = inData[x];
            }

            Save(data);
        }

        unsafe public static void Save(double[] inData)
        {
            int size = (int)Math.Ceiling(Math.Sqrt(inData.Length));

            if(inData.Length != size * size)
                throw new ArgumentException();

            Bitmap image = new Bitmap(size, size, PixelFormat.Format32bppArgb);
            BitmapData imageData = image.LockBits(new System.Drawing.Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

            try
            {
                int* pixels = (int*)imageData.Scan0.ToPointer();
                int imgWidth = image.Width;

                fixed (double* data = inData)
                {
                    double minData = Double.PositiveInfinity, maxData = Double.NegativeInfinity;

                    for (int y = 0, offset = 0; y < size; y++)
                    {
                        for (int x = 0; x < size; x++)
                        {
                            minData = Math.Min(minData, data[offset]);
                            maxData = Math.Max(maxData, data[offset]);

                            offset++;
                        }
                    }

                    for (int y = 0, offset = 0; y < size; y++)
                    {
                        for (int x = 0; x < size; x++)
                        {
                            int rgb = (int)((data[offset] - minData) / (maxData - minData) * 255);

                            pixels[offset] = unchecked(rgb | (rgb << 8) | (rgb << 16) | (int)0xFF000000);

                            offset++;
                        }
                    }
                }
            }
            finally
            {
                image.UnlockBits(imageData);
            }

            image.Save("./test.png");
        }

        unsafe void Blur2D(double[] input, double[] output, double[] filter)
        {
            if ((input.Length != output.Length) || (input.Length != Size * Size))
                throw new ArgumentException();

            // compute filter matrix 
            double[] filterMat = new double[filter.Length * filter.Length];
            double matSum = 0;

            for (int y = 0, offset = 0; y < filter.Length; y++)
            {
                for (int x = 0; x < filter.Length; x++)
                {
                    matSum += filterMat[offset++] = filter[x] * filter[y];
                }
            }

            for (int i = 0; i < filterMat.Length; i++)
            {
                filterMat[i] /= matSum;
            }

            // using unmanaged arrays gives a speedup about 50%...
            fixed (double* pFilterMat = filterMat)
            {
                fixed (double* pInput = input)
                {
                    fixed (double* pOutput = output)
                    {
                        // apply filter to input
                        int filterStride = (int)Math.Floor(filter.Length / 2.0);
                        int size = Size;

                        for (int y = 0; y < size - filter.Length; y++)
                        {
                            for (int x = 0; x < size - filter.Length; x++)
                            {
                                double pixValue = 0;

                                for (int iy = 0, ifilter = 0; iy < filter.Length; iy++)
                                {
                                    for (int ix = 0; ix < filter.Length; ix++)
                                    {
                                        pixValue += pInput[(x + ix) + (y + iy) * size] * pFilterMat[ifilter++];
                                    }
                                }

                                pOutput[(x + filterStride) + (y + filterStride) * size] = pixValue;
                            }
                        }
                    }
                }
            }
        } 
    }
}
